package ar.edu.itba.it.pdc.nio;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import org.apache.log4j.Logger;

import ar.edu.itba.it.pdc.configuration.models.ProxyConfiguration;
import ar.edu.itba.it.pdc.httpParser.ByteBufferQueueImputCharGetter;
import ar.edu.itba.it.pdc.httpParser.HttpParser;
import ar.edu.itba.it.pdc.httpParser.HttpRequestParser;
import ar.edu.itba.it.pdc.httpParser.HttpResponseParser;
import ar.edu.itba.it.pdc.httpParser.Lexer;
import ar.edu.itba.it.pdc.httpParser.exceptions.HttpBadRequestException;
import ar.edu.itba.it.pdc.httpParser.exceptions.HttpMethodNotSuportedException;
import ar.edu.itba.it.pdc.httpParser.exceptions.HttpVersionNotSuportedException;
import ar.edu.itba.it.pdc.httpParser.models.HttpBody;
import ar.edu.itba.it.pdc.httpParser.models.HttpHeaders;
import ar.edu.itba.it.pdc.httpParser.models.HttpMessage;
import ar.edu.itba.it.pdc.httpParser.models.HttpRequest;
import ar.edu.itba.it.pdc.httpParser.models.HttpResponse;
import ar.edu.itba.it.pdc.httpParser.models.HttpStatusLine;
import ar.edu.itba.it.pdc.httpParser.models.HttpVersion;
import ar.edu.itba.it.pdc.httpParser.models.Token;
import ar.edu.itba.it.pdc.monitoring.ProxyMonitoring;
import ar.edu.itba.it.pdc.nio.Connection.ConectionType;

public class UnidirectionalConnection {
	
	private SelectionKey key;
	private SelectionKey Wkey;
	
	private HttpMessage model;

	private ConectionType conectionType;
	
	private HttpRequest request;
		
	private Queue<ByteBuffer> entranceQueue = new LinkedList<ByteBuffer>();
	private Queue<ByteBuffer> exitQueue = new LinkedList<ByteBuffer>();

	private boolean closed = false;
	
	private HttpParser parser;
	private Lexer lexer;

	private String host;
	private int port;
	
	private boolean filterFailure = false;
	
	private Logger logger = Logger.getLogger(ProxySelectorProtocol.class);
	
	public UnidirectionalConnection(ConectionType conectionType, SelectionKey key) {
		this.key = key;
		this.conectionType = conectionType;
		lexer = new Lexer(new ByteBufferQueueImputCharGetter(entranceQueue), conectionType);
		if (conectionType == ConectionType.REQUEST) {
			parser = new HttpRequestParser(lexer);
		}
	}
	
	//Only should be called on responses
	private void setParser() {
			parser = new HttpResponseParser(lexer, request);
	}
	
	public UnidirectionalConnection(){
		conectionType = ConectionType.REQUEST;
	}
	
	public void setWkey(SelectionKey Wkey){
		this.Wkey = Wkey;
	}
	
	public int read(SocketChannel clntChan) throws IOException, MemoryAllocationLimitHited {
		ByteBuffer buf = ByteBufferPool.getInstance().getByteBuffer();
		int bytesRead = clntChan.read(buf);
		buf.flip();
				
		if (bytesRead != -1) {
			if (conectionType == ConectionType.REQUEST) {
				ProxyMonitoring.getInstance().addBytesClientAndProxy(bytesRead);
			} else {
				ProxyMonitoring.getInstance().addBytesServersToProxy(bytesRead);
			}
			entranceQueue.add(buf);
		} else {
			logger.debug("Connection closed while attempting read.");
		}
		return bytesRead;
	}
	

	public void write(SocketChannel clntChan) throws IOException {
		
		int a = clntChan.write(exitQueue.peek());
		if (conectionType == ConectionType.REQUEST) {
			ProxyMonitoring.getInstance().addBytesServersToProxy(a);
		} else {
			ProxyMonitoring.getInstance().addBytesClientAndProxy(a);
		}
		
		if (!exitQueue.peek().hasRemaining()) {
			ByteBufferPool.getInstance().retriveByteBuffer(exitQueue.remove());
		}
	}

	public boolean hasRemaining() {
		return !exitQueue.isEmpty();

	}

	public boolean isClosed() {
		return closed;
	}

	public SelectionKey getKey() {
		return key;
	}

	public SelectionKey getWKey() {
		return Wkey;
	}

	public void close() {
		ProxyMonitoring.getInstance().closeConnection(); 
		closed = true;
		if(lexer != null)
			lexer.connectionClosed();
	}
			
	public InetAddress getIp() throws UnknownHostException{
		if(host == null || host.isEmpty()){
			throw new UnknownHostException();
		}
		return InetAddress.getByName(host);
	}

	public boolean process() throws HttpMethodNotSuportedException, HttpBadRequestException, HttpVersionNotSuportedException, MemoryAllocationLimitHited {
		

		if(parser.parse() && !parser.isTerminated()){
			model = parser.getModel();
			if (conectionType == ConectionType.REQUEST){
				((HttpRequest)model).setOriginIp(((SocketChannel)key.channel()).socket().getInetAddress());
				if (!ProxyConfiguration.getInstance().applyRequestFilters((HttpRequest) model)) {
					filterFailure = true;
				} else {

					model.removeHeader("Proxy-Connection");
					model.removeHeader("TE");
					((HttpRequest)model).removeHostFromSource();
					
					ProxyConfiguration config = ProxyConfiguration.getInstance();
					if ((config.isL33t() || config.isRotateImages())) {
						model.removeHeader("Accept-Encoding");
					} 
					
					model.appendByteBuffers(exitQueue);
					if( model.getHeaderValue("Host").contains(":")){
						host = model.getHeaderValue("Host").split(":")[0];
						try{
						port = Integer.parseInt(model.getHeaderValue("Host").split(":")[1]);
						} catch(NumberFormatException nfe){
							port = 80;
						}
					} else {
						host = model.getHeaderValue("Host");
						port = 80;
					}
					
				}
				
				parser  = new HttpRequestParser(lexer);
				return true;
				
			} else {
				if (!ProxyConfiguration.getInstance().applyResponseFilters(request, (HttpResponse) model)) {
					generateError("403","Forbidden");
					closed = true;
				} else{
					model = ProxyConfiguration.getInstance().applyTransformations((HttpResponse) model);
					
					model.appendByteBuffers(exitQueue);
					host = model.getHeaderValue("Host");
				}
				parser  = new HttpResponseParser(lexer, request);
				return true;
			}
		}
	
		return false;		
	}

	public ConectionType getConnectionType() {
		return conectionType;
	}

	public void generateError(String errorCode, String errorMessage) throws MemoryAllocationLimitHited {
		HttpResponse badRequest = new HttpResponse();
		HttpStatusLine statusLine = new HttpStatusLine();
		statusLine.setVersion(HttpVersion.ONE);
		statusLine.setStatusCode(new Token(errorCode));
		List<Token> code = new LinkedList<Token>();
		code.add(new Token(errorMessage));
		statusLine.setStatusDesc(code);
		badRequest.setInitialLine(statusLine);
		
		HttpHeaders headers = new HttpHeaders();
		headers.put("Connection", "close");
		
		if (errorCode.equals("405")) {
			/* Method Not Allowed 
			 */
			headers.put("Allow", "GET, HEAD, POST");
		}
		badRequest.setHeaders(headers);
		
		badRequest.setBody(new HttpBody());
		
		
		badRequest.appendByteBuffers(exitQueue);
		
	}
	
	public HttpMessage getModel() {
		return model;
	}

	//ONLY SHOULD BE CALLED IF I AM A RESPONSE!
	public void setHttpRequest(HttpRequest request) {
		this.request = request;
		setParser();
	}

	public void retriveBuffers() {
		while(!entranceQueue.isEmpty()){
			ByteBufferPool.getInstance().retriveByteBuffer(entranceQueue.remove());
		}
		while(!exitQueue.isEmpty()){
			ByteBufferPool.getInstance().retriveByteBuffer(exitQueue.remove());
		}
	}

	public boolean filterFailure() {
		return filterFailure;
	}

	public int getPort() {
		return port;
	}
}
